﻿using System;

namespace Lichtspiel.Utility
{
    public class Interpolator<T> : IInterpolator
    {
        /// <summary>
        /// A Method that interpolates a value between start and end.
        /// See InterpolationHelper.
        /// </summary>
        /// <param name="start">Value at amount 0</param>
        /// <param name="end">Value at amount 1</param>
        /// <param name="amount">Amount between 0 and 1</param>
        /// <returns></returns>
        public delegate T InterpolationMethod(T start, T end, float amount);

        private T start;
        private T end;
        private T value;
        private float progress;
        private float speed;        
        private Action<Interpolator<T>> step;
        private Action<Interpolator<T>> completed;
        private InterpolationMethod interpolationMethod;
        private bool valid;
        

        /// <summary>
        /// Internal constructor used by InterpolatorCollection
        /// </summary>
        internal Interpolator() {}

        /// <summary>
        /// Creates a new Interpolator.
        /// </summary>
        /// <param name="startValue">The starting value.</param>
        /// <param name="endValue">The ending value.</param>
        /// <param name="length">The amount of time,  for the interpolation to occur.</param>
        /// <param name="interpolationMethod">How does the interpolation of the value occur.</param>
        /// <param name="step">An optional delegate to invoke each update.</param>        
        /// <param name="completed">An optional delegate to invoke upon completion.</param>
        public Interpolator(T startValue, T endValue, TimeSpan length, InterpolationMethod interpolationMethod,
                            Action<Interpolator<T>> step, Action<Interpolator<T>> completed) {
            Reset(startValue, endValue, length, interpolationMethod, step, completed);
        }

        /// <summary>
        /// Creates a new Interpolator.
        /// </summary>
        /// <param name="startValue">The starting value.</param>
        /// <param name="endValue">The ending value.</param>
        /// <param name="length">The amount of time, for the interpolation to occur.</param>
        /// <param name="interpolationMethod">How does the interpolation of the value occur.</param>
        /// <param name="step">An optional delegate to invoke each update.</param>        
        public Interpolator(T startValue, T endValue, TimeSpan length, InterpolationMethod interpolationMethod,
                            Action<Interpolator<T>> step)
            : this(startValue, endValue, length, interpolationMethod, step, null) {}

        /// <summary>
        /// Gets the interpolator's starting value.
        /// </summary>
        public T Start {
            get { return start; }
        }

        /// <summary>
        /// Gets the interpolator's ending value.
        /// </summary>
        public T End {
            get { return end; }
        }

        /// <summary>
        /// Gets the interpolator's current value.
        /// </summary>
        public T Value {
            get { return value; }
        }

        #region IInterpolator Members

        /// <summary>
        /// Gets whether or not the interpolator is active.
        /// </summary>
        public bool IsActive {
            get { return valid; }
        }

        /// <summary>
        /// Gets the interpolator's progress in the range of [0, 1].
        /// </summary>
        public float Progress {
            get { return progress; }
        }

        /// <summary>
        /// Stops the Interpolator.
        /// </summary>
        public void Stop() {
            valid = false;
            interpolationMethod = null;
            step = null;
            completed = null;
        }

        /// <summary>
        /// Forces the interpolator to set itself to its final position and fire off its delegates before invalidating itself.
        /// </summary>
        public void ForceFinish() {
            if (valid) {
                valid = false;
                progress = 1;
                value = interpolationMethod(start, end, progress);

                if (step != null) {
                    step(this);
                }

                if (completed != null) {
                    completed(this);
                }

                Stop();
            }
        }

        /// <summary>
        /// Updates the Interpolator.
        /// </summary>
        public void Update(GameTime time) {
            if (!valid) {
                return;
            }

            // update the progress, clamping at 1f
            progress = (float) System.Math.Min(progress + speed * time.Elapsed.TotalSeconds, 1f);

            value = interpolationMethod(start, end, progress);

            // invoke the Step callback
            if (step != null) {
                step(this);
            }

            // if the progress is 1...
            if (progress == 1f) {
                // the interpolator is done
                valid = false;

                // invoke the completed callback
                if (completed != null) {
                    completed(this);
                }

                Stop();
            }
        }

        #endregion

        internal void Reset(T startValue, T endValue, TimeSpan length, InterpolationMethod interpolationMethod,
                            Action<Interpolator<T>> step, Action<Interpolator<T>> completed) {
            if (length <= TimeSpan.Zero) {
                throw new ArgumentException("interpolation duration must be greater than zero");
            }

            if (interpolationMethod == null) {
                throw new ArgumentNullException("interpolationMethod");
            }

            valid = true;
            progress = 0f;

            start = startValue;
            end = endValue;
            speed = (float) (1f / length.TotalSeconds);

            this.step = step;
            this.interpolationMethod = interpolationMethod;
            this.completed = completed;
        }
    }
}